// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using ILLink.Shared.TypeSystemProxy;
using Mono.Cecil;

namespace Mono.Linker
{
    internal static class TypeReferenceExtensions
    {
        public static string GetDisplayName(this TypeReference type)
        {
            var builder = GetDisplayNameWithoutNamespace(type);
            var namespaceDisplayName = type.GetNamespaceDisplayName();
            if (!string.IsNullOrEmpty(namespaceDisplayName))
            {
                builder.Insert(0, ".");
                builder.Insert(0, namespaceDisplayName);
            }

            return builder.ToString();
        }

        public static StringBuilder GetDisplayNameWithoutNamespace(this TypeReference type)
        {
            var sb = new StringBuilder();
            if (type == null)
                return sb;

            Stack<TypeReference>? genericArguments = null;
            while (true)
            {
                switch (type)
                {
                    case ArrayType arrayType:
                        AppendArrayType(arrayType, sb);
                        break;
                    case GenericInstanceType genericInstanceType:
                        genericArguments = new Stack<TypeReference>(genericInstanceType.GenericArguments);
                        type = genericInstanceType.ElementType;
                        continue;
                    default:
                        if (type.HasGenericParameters)
                        {
                            int genericParametersCount = type.GenericParameters.Count;
                            int declaringTypeGenericParametersCount = type.DeclaringType?.GenericParameters?.Count ?? 0;

                            string simpleName;
                            if (genericParametersCount > declaringTypeGenericParametersCount)
                            {
                                if (genericArguments?.Count > 0)
                                    PrependGenericArguments(genericArguments, genericParametersCount - declaringTypeGenericParametersCount, sb);
                                else
                                    PrependGenericParameters(type.GenericParameters.Skip(declaringTypeGenericParametersCount).ToList(), sb);

                                int explicitArityIndex = type.Name.IndexOf('`');
                                simpleName = explicitArityIndex != -1 ? type.Name.Substring(0, explicitArityIndex) : type.Name;
                            }
                            else
                                simpleName = type.Name;

                            sb.Insert(0, simpleName);
                            break;
                        }

                        sb.Insert(0, type.Name);
                        break;
                }

                type = type.GetElementType();
                if (type.DeclaringType is not TypeReference declaringType)
                    break;

                type = declaringType;

                sb.Insert(0, '.');
            }

            return sb;
        }

        internal static void PrependGenericParameters(IList<GenericParameter> genericParameters, StringBuilder sb)
        {
            sb.Insert(0, '>').Insert(0, genericParameters[genericParameters.Count - 1]);
            for (int i = genericParameters.Count - 2; i >= 0; i--)
                sb.Insert(0, ',').Insert(0, genericParameters[i]);

            sb.Insert(0, '<');
        }

        static void PrependGenericArguments(Stack<TypeReference> genericArguments, int argumentsToTake, StringBuilder sb)
        {
            sb.Insert(0, '>').Insert(0, genericArguments.Pop().GetDisplayNameWithoutNamespace().ToString());
            while (--argumentsToTake > 0)
                sb.Insert(0, ',').Insert(0, genericArguments.Pop().GetDisplayNameWithoutNamespace().ToString());

            sb.Insert(0, '<');
        }

        static void AppendArrayType(ArrayType arrayType, StringBuilder sb)
        {
            void parseArrayDimensions(ArrayType at)
            {
                sb.Append('[');
                for (int i = 0; i < at.Dimensions.Count - 1; i++)
                    sb.Append(',');

                sb.Append(']');
            }

            sb.Append(arrayType.Name.AsSpan(0, arrayType.Name.IndexOf('[')));
            parseArrayDimensions(arrayType);
            var element = arrayType.ElementType as ArrayType;
            while (element != null)
            {
                parseArrayDimensions(element);
                element = element.ElementType as ArrayType;
            }
        }

        public static TypeReference? GetInflatedDeclaringType(this TypeReference type, ITryResolveMetadata resolver)
        {
            if (type.IsGenericParameter || type.IsByReference || type.IsPointer)
                return null;

            if (type is SentinelType sentinelType)
                return sentinelType.ElementType.GetInflatedDeclaringType(resolver);

            if (type is PinnedType pinnedType)
                return pinnedType.ElementType.GetInflatedDeclaringType(resolver);

            if (type is RequiredModifierType requiredModifierType)
                return requiredModifierType.ElementType.GetInflatedDeclaringType(resolver);

            if (type is GenericInstanceType genericInstance)
            {
                var declaringType = genericInstance.DeclaringType;

                if (declaringType.HasGenericParameters)
                {
                    var result = new GenericInstanceType(declaringType);
                    for (var i = 0; i < declaringType.GenericParameters.Count; ++i)
                        result.GenericArguments.Add(genericInstance.GenericArguments[i]);

                    return result;
                }

                return declaringType;
            }

            if (type is TypeDefinition typeDefinition)
                return typeDefinition.DeclaringType;

            Debug.Assert(false);
            return null;
        }

        public static TypeReference InflateFrom(this TypeReference typeToInflate, IGenericInstance? maybeGenericInstanceProvider)
        {
            if (maybeGenericInstanceProvider is IGenericInstance genericInstanceProvider)
                return InflateGenericType(genericInstanceProvider, typeToInflate);
            return typeToInflate;
        }

        public static IEnumerable<(TypeReference InflatedInterface, InterfaceImplementation OriginalImpl)> GetInflatedInterfaces(this TypeReference typeRef, ITryResolveMetadata resolver)
        {
            var typeDef = resolver.TryResolve(typeRef);

            if (typeDef?.HasInterfaces != true)
                yield break;

            if (typeRef is GenericInstanceType genericInstance)
            {
                foreach (var interfaceImpl in typeDef.Interfaces)
                    yield return (InflateGenericType(genericInstance, interfaceImpl.InterfaceType), interfaceImpl);
            }
            else
            {
                foreach (var interfaceImpl in typeDef.Interfaces)
                    yield return (interfaceImpl.InterfaceType, interfaceImpl);
            }
        }

        public static TypeReference InflateGenericType(IGenericInstance genericInstanceProvider, TypeReference typeToInflate)
        {
            Debug.Assert(genericInstanceProvider is GenericInstanceType or GenericInstanceMethod);
            if (typeToInflate is ArrayType arrayType)
            {
                var inflatedElementType = InflateGenericType(genericInstanceProvider, arrayType.ElementType);

                if (inflatedElementType != arrayType.ElementType)
                    return new ArrayType(inflatedElementType, arrayType.Rank);

                return arrayType;
            }

            if (typeToInflate is GenericInstanceType genericInst)
                return MakeGenericType(genericInstanceProvider, genericInst);

            if (typeToInflate is GenericParameter genericParameter)
            {
                if (genericParameter.Owner is MethodReference)
                {
                    if (genericInstanceProvider is not GenericInstanceMethod)
                        return typeToInflate;
                    return genericInstanceProvider.GenericArguments[genericParameter.Position];
                }

                Debug.Assert(genericParameter.Owner is TypeReference);
                if (genericInstanceProvider is not GenericInstanceType)
                {
                    if (((GenericInstanceMethod)genericInstanceProvider).DeclaringType is not GenericInstanceType genericInstanceType)
                        return typeToInflate;
                    genericInstanceProvider = genericInstanceType;
                }
                return genericInstanceProvider.GenericArguments[genericParameter.Position];
            }

            if (typeToInflate is FunctionPointerType functionPointerType)
            {
                var result = new FunctionPointerType
                {
                    ReturnType = InflateGenericType(genericInstanceProvider, functionPointerType.ReturnType)
                };

                for (int i = 0; i < functionPointerType.Parameters.Count; i++)
                {
                    var inflatedParameterType = InflateGenericType(genericInstanceProvider, functionPointerType.Parameters[i].ParameterType);
                    result.Parameters.Add(new ParameterDefinition(inflatedParameterType));
                }

                return result;
            }

            if (typeToInflate is IModifierType modifierType)
            {
                var modifier = InflateGenericType(genericInstanceProvider, modifierType.ModifierType);
                var elementType = InflateGenericType(genericInstanceProvider, modifierType.ElementType);

                if (modifierType is OptionalModifierType)
                {
                    return new OptionalModifierType(modifier, elementType);
                }

                return new RequiredModifierType(modifier, elementType);
            }

            if (typeToInflate is PinnedType pinnedType)
            {
                var elementType = InflateGenericType(genericInstanceProvider, pinnedType.ElementType);

                if (elementType != pinnedType.ElementType)
                    return new PinnedType(elementType);

                return pinnedType;
            }

            if (typeToInflate is PointerType pointerType)
            {
                var elementType = InflateGenericType(genericInstanceProvider, pointerType.ElementType);

                if (elementType != pointerType.ElementType)
                    return new PointerType(elementType);

                return pointerType;
            }

            if (typeToInflate is ByReferenceType byReferenceType)
            {
                var elementType = InflateGenericType(genericInstanceProvider, byReferenceType.ElementType);

                if (elementType != byReferenceType.ElementType)
                    return new ByReferenceType(elementType);

                return byReferenceType;
            }

            if (typeToInflate is SentinelType sentinelType)
            {
                var elementType = InflateGenericType(genericInstanceProvider, sentinelType.ElementType);

                if (elementType != sentinelType.ElementType)
                    return new SentinelType(elementType);

                return sentinelType;
            }

            return typeToInflate;
        }

        private static GenericInstanceType MakeGenericType(IGenericInstance genericInstanceProvider, GenericInstanceType type)
        {
            var result = new GenericInstanceType(type.ElementType);

            for (var i = 0; i < type.GenericArguments.Count; ++i)
            {
                result.GenericArguments.Add(InflateGenericType(genericInstanceProvider, type.GenericArguments[i]));
            }

            return result;
        }

        public static IEnumerable<MethodReference> GetMethods(this TypeReference type, ITryResolveMetadata resolver)
        {
            TypeDefinition? typeDef = resolver.TryResolve(type);
            if (typeDef?.HasMethods != true)
                yield break;

            if (type is GenericInstanceType genericInstanceType)
            {
                foreach (var methodDef in typeDef.Methods)
                    yield return MakeMethodReferenceForGenericInstanceType(genericInstanceType, methodDef);
            }
            else
            {
                foreach (var method in typeDef.Methods)
                    yield return method;
            }
        }

        private static MethodReference MakeMethodReferenceForGenericInstanceType(GenericInstanceType genericInstanceType, MethodDefinition methodDef)
        {
            var method = new MethodReference(methodDef.Name, methodDef.ReturnType, genericInstanceType)
            {
                HasThis = methodDef.HasThis,
                ExplicitThis = methodDef.ExplicitThis,
                CallingConvention = methodDef.CallingConvention
            };

#pragma warning disable RS0030 // MethodReference.Parameters is banned. It makes sense to use when needing to directly use Cecil's api.
            foreach (var parameter in methodDef.Parameters)
                method.Parameters.Add(new ParameterDefinition(parameter.Name, parameter.Attributes, parameter.ParameterType));
#pragma warning restore RS0030

            foreach (var gp in methodDef.GenericParameters)
                method.GenericParameters.Add(new GenericParameter(gp.Name, method));

            return method;
        }

        public static string ToCecilName(this string fullTypeName)
        {
            return fullTypeName.Replace('+', '/');
        }

        public static bool HasDefaultConstructor(this TypeDefinition type, LinkContext context)
        {
            foreach (var m in type.Methods)
            {
                if (m.HasMetadataParameters())
                    continue;

                var definition = context.Resolve(m);
                if (definition?.IsDefaultConstructor() == true)
                    return true;
            }

            return false;
        }

        public static MethodReference GetDefaultInstanceConstructor(this TypeDefinition type, LinkContext context)
        {
            foreach (var m in type.Methods)
            {
                if (m.HasMetadataParameters())
                    continue;

                var definition = context.Resolve(m);
                if (definition?.IsDefaultConstructor() != true)
                    continue;

                return m;
            }

            throw new NotImplementedException();
        }

        public static bool IsTypeOf(this TypeReference type, string ns, string name)
        {
            return type.Name == name
                && type.Namespace == ns;
        }

        public static bool IsTypeOf(this TypeReference type, string fullTypeName)
        {
            var name = fullTypeName.AsSpan();
            if (type.Name.Length + 1 > name.Length)
                return false;

            if (!name.Slice(name.Length - type.Name.Length).Equals(type.Name.AsSpan(), StringComparison.Ordinal))
                return false;

            if (name[name.Length - type.Name.Length - 1] != '.')
                return false;

            return name.Slice(0, name.Length - type.Name.Length - 1).Equals(type.Namespace, StringComparison.Ordinal);
        }

        public static bool IsTypeOf<T>(this TypeReference tr)
        {
            var type = typeof(T);
            return tr.Name == type.Name && tr.Namespace == type.Namespace;
        }

        public static bool IsTypeOf(this TypeReference tr, WellKnownType type)
        {
            return tr.TryGetWellKnownType() == type;
        }

        public static WellKnownType? TryGetWellKnownType(this TypeReference tr)
        {
            return tr.MetadataType switch
            {
                MetadataType.String => WellKnownType.System_String,
                MetadataType.Object => WellKnownType.System_Object,
                MetadataType.Void => WellKnownType.System_Void,
                // TypeReferences of System.Array do not have a MetadataType of MetadataType.Array -- use string checking instead
                MetadataType.Array or _ => WellKnownTypeExtensions.GetWellKnownType(tr.Namespace, tr.Name)
            };
        }

        public static bool IsSubclassOf(this TypeReference type, string ns, string name, ITryResolveMetadata resolver)
        {
            TypeDefinition? baseType = resolver.TryResolve(type);
            while (baseType != null)
            {
                if (baseType.IsTypeOf(ns, name))
                    return true;
                baseType = resolver.TryResolve(baseType.BaseType);
            }

            return false;
        }

        public static TypeReference WithoutModifiers(this TypeReference type)
        {
            while (type is IModifierType)
            {
                type = ((IModifierType)type).ElementType;
            }
            return type;
        }

        // Check whether this type represents a "named type" (i.e. a type that has a name and can be resolved to a TypeDefinition),
        // not an array, pointer, byref, or generic parameter. Conceptually this is supposed to represent the same idea as Roslyn's
        // INamedTypeSymbol, or ILC's DefType/MetadataType.
        public static bool IsNamedType(this TypeReference typeReference)
        {
            if (typeReference.IsRequiredModifier)
                return ((RequiredModifierType)typeReference).ElementType.IsNamedType();
            if (typeReference.IsOptionalModifier)
                return ((OptionalModifierType)typeReference).ElementType.IsNamedType();

            if (typeReference.IsDefinition || typeReference.IsGenericInstance)
                return true;

            if (typeReference.IsArray ||
                typeReference.IsByReference ||
                typeReference.IsPointer ||
                typeReference.IsFunctionPointer ||
                typeReference.IsGenericParameter)
                return false;

            // Shouldn't get called for these cases
            Debug.Assert(!typeReference.IsPinned);
            Debug.Assert(!typeReference.IsSentinel);
            if (typeReference.IsPinned || typeReference.IsSentinel)
                return false;

            Debug.Assert(typeReference.GetType() == typeof(TypeReference));
            return true;
        }

        /// <summary>
        /// Resolves a TypeReference to a TypeDefinition if possible. Non-named types other than arrays (pointers, byrefs, function pointers) return null.
        /// Array types that are dynamically accessed resolve to System.Array instead of its element type - which is what Cecil resolves to.
        /// Any data flow annotations placed on a type parameter which receives an array type apply to the array itself. None of the members in its
        /// element type should be marked.
        /// </summary>
        public static TypeDefinition? ResolveToTypeDefinition(this TypeReference typeReference, LinkContext context)
            => typeReference is ArrayType
                ? BCL.FindPredefinedType(WellKnownType.System_Array, context)
                : typeReference.IsNamedType()
                    ? context.TryResolve(typeReference)
                    : null;

        public static bool IsByRefOrPointer(this TypeReference typeReference)
        {
            return typeReference.WithoutModifiers().MetadataType switch
            {
                MetadataType.Pointer or MetadataType.ByReference => true,
                _ => false,
            };
        }
    }
}
